From d186a989b6808ff39fcd43a9f0b478aebc4aa346 Mon Sep 17 00:00:00 2001 From: zwlucas Date: Fri, 29 May 2026 17:21:48 -0300 Subject: feat: enhance maintenance management and heartbeat history features - Updated Store interface to include methods for managing maintenances. - Modified API functions to support pagination for heartbeat history and added CRUD operations for maintenances. - Enhanced types to include paginated responses and initial log entries. - Implemented maintenance management UI in the admin panel with create, edit, and delete functionalities. - Updated service detail page to display paginated heartbeat history and improved chart rendering. - Refactored status page to accommodate new data structures and ensure consistent data handling. Signed-off-by: Lucas Faria Mendes --- frontend/src/routes/services/[id]/+page.svelte | 287 +++++++++++++++++++------ 1 file changed, 225 insertions(+), 62 deletions(-) (limited to 'frontend/src/routes/services/[id]/+page.svelte') diff --git a/frontend/src/routes/services/[id]/+page.svelte b/frontend/src/routes/services/[id]/+page.svelte index 36ea105..a7b8877 100644 --- a/frontend/src/routes/services/[id]/+page.svelte +++ b/frontend/src/routes/services/[id]/+page.svelte @@ -6,17 +6,41 @@ import type { Service, Heartbeat, ServiceStats } from '$lib/types'; let svc: Service | undefined = $state(); - let history: Heartbeat[] = $state([]); + let chartHistory: Heartbeat[] = $state([]); let stats: ServiceStats | null = $state(null); let loading = $state(true); + + let tableData: Heartbeat[] = $state([]); + let currentPage = $state(1); + let totalPages = $state(1); + let totalItems = $state(0); + let expandedError: number | null = $state(null); + const PER_PAGE = 50; + + async function loadTablePage(pageNum: number) { + const res = await getHistory(Number($page.params.id), pageNum, PER_PAGE); + tableData = res.data; + currentPage = res.page; + totalPages = res.total_pages; + totalItems = res.total; + } + onMount(async () => { try { const all = await listServices(); svc = all.find((s) => s.id === Number($page.params.id)); - history = await getHistory(Number($page.params.id)); + + const chartRes = await fetch(`/api/services/${$page.params.id}/history?page=1&per_page=50`); + if (chartRes.ok) { + const json = await chartRes.json(); + chartHistory = json.data ?? json; + } + getServiceStats(Number($page.params.id)).then((s) => (stats = s)).catch(() => {}); + + await loadTablePage(1); } catch { // handle } finally { @@ -24,47 +48,117 @@ } }); - $effect(() => { - if (!svc && $page.params.id) { - getHistory(Number($page.params.id)) - .then((h) => (history = h)) - .catch(() => {}); - } - }); - onMount(() => { const unsub = subscribeHeartbeats((hb) => { if (hb.service_id !== Number($page.params.id)) return; - history = [...history, hb].slice(-50); + chartHistory = [...chartHistory, hb].slice(-50); + if (currentPage === 1) { + tableData = [hb, ...tableData].slice(0, PER_PAGE); + totalItems++; + totalPages = Math.max(1, Math.ceil(totalItems / PER_PAGE)); + } }); return unsub; }); - let chartPad = { t: 8, b: 20, l: 40, r: 12 }; - let chartW = $derived(Math.max(history.length * 32, 300)); + let containerWidth = $state(0); + let pad = { t: 8, b: 24, l: 44, r: 12 }; let chartH = 200; - let plotW = $derived(chartW - chartPad.l - chartPad.r); - let plotH = $derived(chartH - chartPad.t - chartPad.b); - let chartMax = $derived(Math.max(...history.map((h) => h.response_time_ms), 100)); - let chartMin = $derived(0); + let plotW = $derived(Math.max(containerWidth - pad.l - pad.r, 0)); + let plotH = $derived(chartH - pad.t - pad.b); + + function niceNum(range: number, round: boolean): number { + const exp = Math.floor(Math.log10(range)); + const frac = range / Math.pow(10, exp); + let nice: number; + if (round) { + if (frac <= 1.5) nice = 1; + else if (frac <= 3) nice = 2; + else if (frac <= 7) nice = 5; + else nice = 10; + } else { + if (frac <= 1) nice = 1; + else if (frac <= 2) nice = 2; + else if (frac <= 5) nice = 5; + else nice = 10; + } + return nice * Math.pow(10, exp); + } + + let yAxis = $derived.by(() => { + const vals = chartHistory.map((h) => h.response_time_ms); + if (vals.length === 0) return { min: 0, max: 100, ticks: [{ val: 0, y: 0 }, { val: 100, y: 200 }] }; + const rawMin = Math.min(...vals); + const rawMax = Math.max(...vals); + if (rawMax === rawMin) return { min: 0, max: Math.max(rawMax * 2, 100), ticks: [] }; + + const range = rawMax - rawMin; + const pad = Math.max(range * 0.15, 10); + let lo = Math.max(0, rawMin - pad); + let hi = rawMax + pad; + + const tickStep = niceNum((hi - lo) / 4, true); + lo = Math.floor(lo / tickStep) * tickStep; + hi = Math.ceil(hi / tickStep) * tickStep; + + const ticks: { val: number; y: number }[] = []; + for (let v = lo; v <= hi + tickStep * 0.001; v += tickStep) { + const y = pad.t + plotH - ((v - lo) / (hi - lo || 1)) * plotH; + ticks.push({ val: Math.round(v), y }); + } + return { min: lo, max: hi, ticks }; + }); + + let segments = $derived.by(() => { + const n = chartHistory.length; + if (n < 2) return []; + const { min, max } = yAxis; + return chartHistory.slice(0, -1).map((h, i) => { + const next = chartHistory[i + 1]; + const x1 = pad.l + (i / (n - 1)) * plotW; + const x2 = pad.l + ((i + 1) / (n - 1)) * plotW; + const y1 = pad.t + plotH - ((h.response_time_ms - min) / (max - min || 1)) * plotH; + const y2 = pad.t + plotH - ((next.response_time_ms - min) / (max - min || 1)) * plotH; + return { + x1, y1, x2, y2, + color: h.is_up && next.is_up ? 'var(--green)' : 'var(--red)' + }; + }); + }); let dots = $derived( - history.map((h, i) => { - const x = history.length === 1 - ? chartPad.l + plotW / 2 - : chartPad.l + (i / (history.length - 1)) * plotW; - const y = chartPad.t + plotH - ((h.response_time_ms - chartMin) / (chartMax - chartMin || 1)) * plotH; + chartHistory.map((h, i) => { + const { min, max } = yAxis; + const x = pad.l + (i / (Math.max(chartHistory.length - 1, 1))) * plotW; + const y = pad.t + plotH - ((h.response_time_ms - min) / (max - min || 1)) * plotH; return { x, y, ...h }; }) ); - let yTicks = $derived.by(() => { - const n = 4; - return Array.from({ length: n + 1 }, (_, i) => { - const val = chartMin + ((chartMax - chartMin) / n) * i; - const y = chartPad.t + plotH - ((val - chartMin) / (chartMax - chartMin || 1)) * plotH; - return { val: Math.round(val), y }; - }); + let areaPath = $derived.by(() => { + if (chartHistory.length < 2) return ''; + const { min, max } = yAxis; + const baseline = pad.t + plotH; + const top = dots.map((d) => `${d.x},${d.y}`).join(' L '); + const bottom = dots.map((d) => d.x).reverse().map((x) => `${x},${baseline}`).join(' L '); + return `M ${top} L ${bottom} Z`; + }); + + let pages = $derived.by(() => { + const p: (number | string)[] = []; + const total = totalPages; + if (total <= 7) { + for (let i = 1; i <= total; i++) p.push(i); + } else { + p.push(1); + if (currentPage > 3) p.push('...'); + const start = Math.max(2, currentPage - 1); + const end = Math.min(total - 1, currentPage + 1); + for (let i = start; i <= end; i++) p.push(i); + if (currentPage < total - 2) p.push('...'); + p.push(total); + } + return p; }); @@ -143,39 +237,47 @@

Tempo de Resposta (ms)

-
- {#if history.length === 0} +
+ {#if chartHistory.length === 0}
Sem dados ainda
- {:else if history.length === 1} + {:else if chartHistory.length === 1}
- {history[0].response_time_ms} + {chartHistory[0].response_time_ms} ms - {history[0].is_up ? 'Online' : 'Offline'} — - {history[0].status_code || 'timeout'} + {chartHistory[0].is_up ? 'Online' : 'Offline'} — + {chartHistory[0].status_code || 'timeout'}
{:else} - - {#each yTicks as tick} + + + + + + + + + + {#each yAxis.ticks as tick} {/each} - `${d.x},${d.y}`).join(' ')} - fill="none" - stroke="var(--green)" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" - /> + + + + {#each segments as seg} + + {/each} + + {#each dots as d} - + {/each} + + +
+ + {new Date(chartHistory[0].tested_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} + + + {new Date(chartHistory[chartHistory.length - 1].tested_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} + +
{/if}
-
- {history[0]?.tested_at ? new Date(history[0].tested_at).toLocaleTimeString() : ''} - {history[history.length - 1]?.tested_at - ? new Date(history[history.length - 1].tested_at).toLocaleTimeString() - : ''} -
- -
+ +

- Últimas Verificações + Histórico de Verificações

+
- {#each history.slice().reverse() as h} + {#each tableData as h}
@@ -256,6 +376,49 @@
{/each}
+ + + {#if totalPages > 1} +
+ + + {#each pages as p} + {#if p === '...'} + + {:else} + + {/if} + {/each} + + +
+ +

+ Página {currentPage} de {totalPages} — {totalItems} verificações no total +

+ {/if}
{/if} -- cgit v1.2.3